Skip to content

Static tree pictures: renderTreeSvg and glpython-tree - #54

Merged
cedricvogel-leo merged 2 commits into
mainfrom
feature/tree-svg
Nov 6, 2026
Merged

cedricvogel-leo merged 2 commits into
mainfrom
feature/tree-svg

Conversation

@cedricvogel-leo

@cedricvogel-leo cedricvogel-leo commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

P43: a list that reads as a tree, drawn as a static SVG file in Node, without a browser, for a compile step (slides, solutions, quiz options, print).

What it does

  • renderTreeSvg(text, options) → string (src/treeSvg.ts):
    • Input: a list as Python writes it: numbers (negative and decimal), strings in single or double quotes with Python's escapes, True, False, None, lists and tuples. Leaves are written with Python's repr (2.50 → 2.5, "it's", 1e+16). A single value is a tree of one leaf.
    • Tree rule: the runtime's listtree rule (_glpython_tree_of): a first element that is a string is the label, without quotes. Unlike the visualizer, long texts aren't shortened.
    • Layout: layoutTree from treeLayout.ts, so the positions are the ones TreeView uses.
    • Look: TreeView's, with the light theme's colours. Solid node boxes, dashed leaf boxes, string and number colours, and the same edges, spacing and radius.
    • Background: a white card with a grey border, rounded corners and 12 px padding, so the picture looks the same on a light and a dark page.
    • Font: the code font stack, which ends in monospace, at 16 px by default. Widths are in characters (0.6 em each), and each text is fitted to that width with textLength, so it doesn't overflow its box in a different monospace font.
    • mark: each marked subtree has its boxes and the edges between them in the accent colour, with the boxes filled --glp-current. Marking the whole tree marks all of it. A mark that occurs twice or not at all is an error.
    • Byte-identical: numbers have at most two decimals, the element and attribute order is fixed, and there are no ids or dates.
    • markRoot (--mark-root): the root node's box alone, not its edges or children, in the same look as a marked subtree's boxes. It combines with mark, and on a single value it marks that leaf. The description names it first: «– markiert: Wurzel» / «– marked: root», e.g. «– markiert: Wurzel; ['*', 3, 4]».
    • Accessible: role="img" and a <title> with the list, plus – marked: … (lang: 'en') or – markiert: … (lang: 'de').
    • Errors: a TreeSvgError naming the position with a caret, or explaining why a mark is ambiguous or missing.
  • glpython-tree (bin/glpython-tree.mjs):
    • glpython-tree "<list>" [--mark "<subtree>"]... [--lang en|de] [--font-size <px>] [-o file.svg];
    • writes to stdout without -o, and reads the list from stdin without an argument;
    • exits with 1 on an error, 2 on wrong usage.
  • Packaging:
    • Separate entry: dist/tree-svg.js, exported as glpython-editor/tree-svg, with no React or Monaco; it shares the layout chunk with dist/index.js.
    • Optional peer dependencies: peerDependenciesMeta makes them optional, so a repo installing the package only for the pictures doesn't get React or Monaco installed. I checked this with a git+file install in an empty project: only glpython-editor was installed, and both the import and npx glpython-tree work.

Checks

npm run test:svg (new, in CI after the build):

  • Snapshots: the eight trees of the layout check, a marked subtree, the whole tree marked, a single value, and markRoot alone, with a subtree mark and on a single value, against snapshots in scripts/tree-svg/, byte for byte and the same twice.
  • Positions: every box where treeLayout places its node.
  • Repr: a set of values against CPython's own repr, including floats, big ints, quotes, escapes and tuples.
  • Description: role="img", the <title>, and escaped XML.
  • Errors: six kinds of invalid input with their positions, plus a mark twice, missing and invalid.
  • The command: against the build, with stdout, stdin and both exit codes.

Also checked:

  • Look: compared with TreeView in the embed's REPL at the same font size (15 px), side by side; they look the same.
  • Other checks: lint (the known warning only), test:protocol and test:layout pass.

How to use it from another repo

npm install --save-dev git+https://github.com/glpython-edu/glpython-editor.git#v0.1.28
import { renderTreeSvg, TreeSvgError } from 'glpython-editor/tree-svg'
const svg = renderTreeSvg("['+', 2, ['*', 3, 4]]", { mark: ["['*', 3, 4]"], lang: 'de' })

The command is then available as npx glpython-tree …. Installing runs prepare, which builds dist/, as for the IDE. That takes a moment and needs no TypeScript setup on the course side.

Going live

Nothing changes for the embed or the IDE: the workspace bundle is unchanged apart from sharing the layout chunk. A tag (e.g. v0.1.28) on the merge commit makes it installable.

Follow-ups (not done)

  • Fonts: a picture opened on a system without any monospace font of normal proportions can look squeezed, because textLength fits the text to its box.
  • Dark variant: the colours are the light tokens written into the file; there is no dark variant (the brief asked for a light card).
  • Dicts and sets aren't accepted as input. As leaves, they'd need Python's repr of a dict.
  • Mark matching is by repr: 1.0 doesn't find 1, unlike Python's ==.

🤖 Generated with Claude Code

cedricvogel-leo and others added 2 commits November 6, 2026 14:57
A list that reads as a tree, drawn as an SVG file in Node without a
browser: the runtime's tree rule, treeLayout's positions and TreeView's
look on a light card; marks for subtrees; byte-identical output; a
<title> with the list. Parses Python's literal syntax, writes leaves
with Python's repr. Built as its own entry (glpython-editor/tree-svg,
no React or Monaco), with the command glpython-tree; the peer
dependencies are optional, so a repo using only this doesn't get React.

Checks: test:svg (snapshots, positions, CPython's repr, errors, the
command).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
markRoot (--mark-root) highlights only the root's box, not its edges
or children, with the same look as a marked subtree's boxes; it
combines with mark and marks a single value's leaf. The description
names it first: «– markiert: Wurzel» / «– marked: root». Snapshots for
it alone, with a subtree, and on a single value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cedricvogel-leo
cedricvogel-leo merged commit 1e21b7c into main Nov 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant